# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: jaewon

import datetime
import logging
import os
import re
import sys

from absl import app, flags

from coin.strategy.tool.log_v1.util import find_log_pos_by_timestamp
from coin.strategy.tool.log_v2.util import extract_datetime


def parse_datetime_str(datetime_str):
  if datetime_str is None:
    return None
  try:
    return datetime.datetime.strptime(datetime_str, '%Y%m%d-%H%M%S')
  except ValueError:
    return datetime.datetime.strptime(datetime_str, '%Y%m%d')


class _ParseFinished(Exception):
  pass


class Bitmex503Dumper(object):
  def __init__(self, log_path, begin_time=None, end_time=None):
    self._log_path = log_path
    self._begin_time = begin_time
    self._end_time = end_time

    self._line_counter = 0

    self._in_error_msg = False
    self._error_time = None
    self._error_msg_line_counter = 0
    self._error_timestamps = []

  def parse_regular(self, line):
    if 'Order error' in line or 'Exception in callback' in line:
      self._error_time = extract_datetime(line)
      if self._error_time is None:
        return
      if self._end_time and self._error_time >= self._end_time:
        raise _ParseFinished()
      self._in_error_msg = True
      self._exception_msg_line_counter = 0
      return

    if self._end_time and self._line_counter % 100 == 0:
      line_time = extract_datetime(line)
      if line_time and line_time >= self._end_time:
        raise _ParseFinished()

  def parse_error_msg(self, line):
    err_msg = '503 Server Error: Service Unavailable for url: https://www.bitmex.com/api/v1/order'
    out_error_msg = False

    if err_msg in line:
      self._error_timestamps.append(self._error_time)
      out_error_msg = True

    self._error_msg_line_counter += 1
    if self._error_msg_line_counter >= 20:
      out_error_msg = True

    if out_error_msg:
      self._error_time = None
      self._error_msg_line_counter = 0
      self._in_error_msg = False

  def run(self):
    pos = 0
    if self._begin_time:
      pos = find_log_pos_by_timestamp(self._log_path, self._begin_time, False)

    with open(self._log_path, 'r') as log_file:
      if pos:
        log_file.seek(pos)

      try:
        for line in log_file.readlines():
          line = line.strip()
          if not self._in_error_msg:
            self.parse_regular(line)
          else:
            self.parse_error_msg(line)
          self._line_counter += 1
      except _ParseFinished:
        pass

    return self._error_timestamps


def extract_503_timestamps(log_path, begin_time, end_time):
  dumper = Bitmex503Dumper(log_path, begin_time, end_time)
  return dumper.run()


def extract_503_timestamps_from_multiple_logs(paths, begin_time, end_time, exclude_regex=None):
  error_timestamps = []
  for path in paths:
    path = os.path.normpath(os.path.expanduser(path))
    if os.path.isfile(path):
      if exclude_regex and re.search(exclude_regex, path):
        logging.info('Skip %s', path)
        continue
      logging.info('Processing %s', path)
      error_timestamps += extract_503_timestamps(path, begin_time, end_time)

    elif os.path.isdir(path):
      for dirpath, dirnames, filenames in os.walk(path):
        for filename in filenames:
          if not filename.startswith('log_'):
            continue

          log_path = os.path.join(dirpath, filename)
          if exclude_regex and re.search(exclude_regex, log_path):
            logging.info('Skip %s', log_path)
            continue

          logging.info('Processing %s', log_path)
          error_timestamps += extract_503_timestamps(log_path, begin_time, end_time)

  return sorted(set(error_timestamps))


def generate_intervals(error_timestamps, margin_left=None, margin_right=None):
  margin_left = margin_left or datetime.timedelta(seconds=2)
  margin_right = margin_right or datetime.timedelta(seconds=6)
  assert margin_left >= datetime.timedelta()
  assert margin_right >= datetime.timedelta()

  intervals = []
  first_ts = None
  last_ts = None

  for ts in error_timestamps + [datetime.datetime.max]:
    if last_ts and last_ts + margin_right < ts - margin_left:
      intervals.append((first_ts - margin_left, last_ts + margin_right))
      first_ts, last_ts = None, None

    if first_ts is None:
      first_ts = ts
    last_ts = ts

  return intervals


def merge_intervals(intervals_1, intervals_2):
  # TODO(jaewon): Make it O(n)
  DT_MIN = datetime.datetime.min
  DT_MAX = datetime.datetime.max

  intervals = sorted(intervals_1 + intervals_2)
  merged = []
  first_ts, last_ts = None, None
  for begin, end in intervals + [(DT_MAX, DT_MAX)]:
    if last_ts and last_ts < begin:
      merged.append((first_ts, last_ts))
      first_ts, last_ts = None, None

    if first_ts is None:
      first_ts = begin
    last_ts = max(last_ts or DT_MIN, end)

  return merged


DT_FORMAT = '%Y-%m-%dT%H:%M:%S.%f'


def load_intervals(filepath):
  intervals = []
  with open(filepath, 'r') as f:
    for line in f.readlines():
      begin_str, end_str = line.strip().split(',')
      begin = datetime.datetime.strptime(begin_str, DT_FORMAT)
      end = datetime.datetime.strptime(end_str, DT_FORMAT)
      intervals.append((begin, end))
  return intervals


def main(argv):
  FLAGS = flags.FLAGS
  if len(argv) <= 2:
    sys.stderr.write('Usage: <program> <command> <files or dirs> ...\n' 'Command: dump, interval\n')
    return 1

  command = argv[1]
  assert command in ('dump', 'interval')
  begin_time = parse_datetime_str(FLAGS.begin_time)
  end_time = parse_datetime_str(FLAGS.end_time)

  error_timestamps = extract_503_timestamps_from_multiple_logs(argv[2:],
                                                               begin_time,
                                                               end_time,
                                                               FLAGS.exclude_regex)

  if command == 'dump':
    for ts in error_timestamps:
      print(ts)

  elif command == 'interval':
    prev_intervals = []
    if FLAGS.merge_with:
      prev_intervals = load_intervals(FLAGS.merge_with)
    intervals = generate_intervals(error_timestamps)
    intervals = merge_intervals(prev_intervals, intervals)

    if FLAGS.human_readable:
      for begin, end in intervals:
        print('%s - %s (%s)' % (begin, end, end - begin))
    else:
      for begin, end in intervals:
        print('%s,%s' % (begin.strftime(DT_FORMAT), end.strftime(DT_FORMAT)))

  return 0


if __name__ == '__main__':
  flags.DEFINE_string('begin_time', None, 'Begin time in YYYYMMDD-HHMMSS or YYYYMMDD format')
  flags.DEFINE_string('end_time', None, 'End time in YYYYMMDD-HHMMSS or YYYYMMDD format')
  flags.DEFINE_string('merge_with',
                      None,
                      'If specified, it merges with specified file. Interval only.')
  flags.DEFINE_bool('human_readable',
                    True,
                    'If true, it dumps in human readable format. Interval only.')
  flags.DEFINE_string('exclude_regex',
                      None,
                      'If specified, it excludes files matched by the specified pattern.')
  app.run(main)
